34. Spring6 AOP

简单计算器

这里简单的实现一个计算器的 demo,最终的目的是希望在计算前后输出日志。

1
2
3
4
5
6
7
8
9
package com.itguigu.proxy;

public interface MathI {

int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.itguigu.proxy;

public class MathImpl implements MathI{

@Override
public int add(int i, int j) {
System.out.println("add 接收到参数:" + i + "," + j);
int result = i + j;
System.out.println("add 结果返回:" + result);
return result;
}

@Override
public int sub(int i, int j) {
System.out.println("sub 接收到参数:" + i + "," + j);
int result = i - j;
System.out.println("sub 结果返回:" + result);
return result;
}

@Override
public int mul(int i, int j) {
System.out.println("mul 接收到参数:" + i + "," + j);
int result = i * j;
System.out.println("mul 结果返回:" + result);
return result;
}

@Override
public int div(int i, int j) {
System.out.println("div 接收到参数:" + i + "," + j);
int result = i / j;
System.out.println("div 结果返回:" + result);
return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.itguigu.proxy;

public class Test {
public static void main(String[] args) {
MathI mathI = new MathImpl();
int add = mathI.add(1, 1);
System.out.println(add);
}
}
/**
* add 接收到参数:1,1
add 结果返回:2
2
*/

上面的每个操作的输出都显得非常的麻烦,可以考虑使用代理来优化。

动态代理

新建动态代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.itguigu.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
* 动态代理工具类
* @author rex
*
*/
public class ProxyUtil{
// 目标对象(被代理的对象)
private MathImpl mathImpl;

// 创建有参构造为成员变量 mathImpl 赋值
public ProxyUtil(MathImpl mathImpl) {
super();
this.mathImpl = mathImpl;
}

// 获取代理对象
public Object getProxy() {
/**
* 借助 Proxy.newProxyInstance 来返回一个代理类对象
* 第一个参数是类加载器,可以是被代理类 MathImpl 的,也可以是 当前类 ProxyUtil 的。只要是个类加载器就行
* 第二个参数是被代理内的接口们,只有知道被代理类有哪些接口,才能创建除代理类对象(中介得知道你的找房需求才能代替给你找房)
* 第三个参数是 InvocationHandler 对象,这里使用匿名内部类实现,实现 InvocationHandler 需要重写它的 invoke 方法
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = mathImpl.getClass().getInterfaces();

return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MyLogger.before(method.getName(), Arrays.toString(args));
Object result = method.invoke(mathImpl, args);
MyLogger.after(method.getName(), result);
return result;
}
});
}
}

新建 MyLogger 类

1
2
3
4
5
6
7
8
9
10
11
package com.itguigu.proxy;

public class MyLogger {
public static void before(String methodName, String args) {
System.out.println("方法 - " + methodName + " args:" + args);
}

public static void after(String methodName, Object result) {
System.out.println("方法 - " + methodName + " result:" + result);
}
}

使用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itguigu.proxy;

public class Test {
public static void main(String[] args) {
// MathI mathI = new MathImpl();
// int add = mathI.add(1, 1);
// System.out.println(add);

ProxyUtil paProxyUtil = new ProxyUtil(new MathImpl());

// 这里不能将 getProxy 获取到的动态代理对象强制转换为目标对象。得把它转换为相对应的接口类型。
// 因为动态代理是根据接口动态的创建出一个代理类对象,这里生成的代理类对象和目标对象(被代理的对象)其实相当于是兄弟关系,所以不能相互转换。
// 或者这样理解,如果这里强转成了 目标对象,那么无法保证后续调用的方法还是接口里面需要代理的(可能不调用加减乘除,调用 xxx 方法,这样的话代理就没用)
// 所以这里只能强转为接口对象,因为接口里面就只能调用需要被代理的方法(加减乘除)
MathI mathI = (MathI) paProxyUtil.getProxy();
mathI.add(12, 12);
/**
* 方法 - add args:[12, 12]
方法 - add result:24
*/
}
}

代码地址

AOP

AOP(Aspect-Oriented Programming,面向切面编程 ) 是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。AOP 编程操作的主要对象是切面 (aspect),而切面用于模块化横切关注点(公共功能)

AspectJ 在 Spring 中 AOP 的实现

导入JAR包

1
2
3
4
5
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar

同样的实现之前的 MathI 接口和 MathImpl 实现类。并将 MathImpl 加上 @Component 注解

1
2
3
4
5
6
7
8
package com.atguigu.spring.aop;

public interface MathI {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.atguigu.spring.aop;

import org.springframework.stereotype.Component;

@Component
public class MathImpl implements MathI{

@Override
public int add(int i, int j) {
int result = i + j;
return result;
}

@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}

@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}

@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}

定义一个类,加上 @Aspect 使其成为一个切面,并在其中使用 @Before 定义一个前置通知。并加上切入点表达式。并将改类加上 @Component 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.spring.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect // 这个注解的作用是标示当前类为切面
public class MyLoggerAspect {

/**
* 将方法指定为前置通知。
* 必须设置 value,值为切入点表达式(解析这个表达式,将通知作用于这个表达式代表的位置)
* 也就说将 befordMethod 这个方法作用到 add 方法执行之前。
*/
@Before(value = "execution(public int com.atguigu.spring.aop.MathImpl.add(int, int))")
public void befordMethod() {
System.out.println("前置通知");
}
}

新增 xml 配置文件,添加 spring 扫描,开启 aspectj 的自动代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

<!-- spring 扫描 -->
<context:component-scan base-package="com.atguigu.spring.aop"></context:component-scan>

<!-- 开启 aspectj 的自动代理功能 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

最后测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.spring.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("aop.xml");
// 这里获取的对象需要转换为 MathI,原因和直接介绍 aop 的时候一样,如果是 mathImpl 就是兄弟关系了
MathI mathI = applicationContext.getBean("mathImpl", MathI.class);
int add = mathI.add(1, 1);
System.out.println(add);
}
}
切入点表达式

上面的例子中,只为了 add 方法添加前置通知,如果想为 MathImpl 接口下的所有方法都添加前置通知,那么切入点表达式应该如下

1
@Before(value = "execution(public int com.atguigu.spring.aop.MathImpl.*(int, int))")

MathImpl 接口下的所有方法,且任何访问修饰符和返回值都可以

1
@Before(value = "execution(* com.atguigu.spring.aop.MathImpl.*(int, int))")

MathImpl 接口下的所有方法,且可以是任何访问修饰符和返回值,可以是 aop 包下的任意的类,以及任意的参数。(即第一个 表示任意的访问修饰符和返回值,第二个 代表 aop 下任意的类,第三个 * 代表任意的方法,.. 代表任意的参数。)

1
@Before(value = "execution(* com.atguigu.spring.aop.*.*(..))")

为了验证上面切入点表达式,我们在 aop 下新建一个 TestHandler 类,并在里面创建一个无参的 test 方法。【注意⚠️:默认 JDK 的 AOP 实现是需要接口的,但是这里没有,原因是我们使用了 cglib,有了 cglib,那么依靠类的继承也能实现 AOP,TestHandler 默认是继承 Object 的。】

1
2
3
4
5
6
7
8
9
10
package com.atguigu.spring.aop;

import org.springframework.stereotype.Component;

@Component
public class TestHandler {
public void test() {
System.out.println("测试切入点表达式");
}
}

测试效果

1
2
3
4
5
6
7
8
// 测试切入点表达式
TestHandler bean = applicationContext.getBean("testHandler", TestHandler.class);
bean.test();
/**
* method: test ,args: []
前置通知
测试切入点表达式
*/
前置通知

我们在 @Before 的 value 中传入了切入点表达式,所以我们也能在前置通知的方法参数重获取到切入点对象,从而获取到被代理方法的一些信息。@Before 作用于方法执行之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.atguigu.spring.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect // 这个注解的作用是标示当前类为切面
public class MyLoggerAspect {

/**
* 将方法指定为前置通知。
* 必须设置 value,值为切入点表达式(解析这个表达式,将通知作用于这个表达式代表的位置)
* 也就说将 befordMethod 这个方法作用到 add 方法执行之前。
*/
@Before(value = "execution(public int com.atguigu.spring.aop.MathImpl.add(int, int))")
public void befordMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs(); // 获取方法参数
String name = joinPoint.getSignature().getName(); // 获取方法名
System.out.println("method: " + name + " ,args: " + Arrays.toString(args));
System.out.println("前置通知");
}
}
后置通知

@After 将方法标注为后置通知。作用于方法的 finally 语句块,相当于不管有没有异常都会执行。

1
2
3
4
5
6
7
8
  /**
* @After 将方法标注为后置通知。相当于方法的 finally 语句块,相当于不管有没有异常都会执行。
*/
@After(value = "execution (* com.atguigu.spring.aop.*.*(..))")
public void afterMethos() {
System.out.println("后置通知");
}
}
返回通知 (最终通知)

@AfterReturning将方法标注为返回通知。相当于方法正确执行(无异常)之后。可以通过 returning 设置接收方法返回值的变量名(也就是说设置一个变量名称,用来接收方法的返回值,且这个返回值是 Object 类型的)。如果返回值要在方法中使用,那么必须在方法的形参中设置一个参数,名字和 returning 设置的变量名一致。

1
2
3
4
5
6
7
8
9
10

/**
* @AfterReturning 将方法标注为返回通知。相当于方法正确执行(无异常)之后。
* 可以通过 returning 设置接收方法返回值的变量名。如果要在方法中使用,必须在方法的形参中设置一个参数,名 字和 returning 设置的变量名一致。
*/
@AfterReturning(value = "execution (* com.atguigu.spring.aop.*.*(..))", returning = "result")
public void afterReturningMethos(JoinPoint joinPoint, Object result) {
System.out.println("method: " + joinPoint.getSignature().getName() + " ,result: " + result);
System.out.println("返回通知");
}
异常通知

@AfterThrowing 将方法标注为异常通知(例外通知)作用于方法抛出异常时, 可以通过 throwing 设置接收方法异常的信息, 在参数列表中可以通过具体的异常类型来对指定的异常信息进行操作。例如这里是 NullPointerException 异常。即当发生 NullPointerException 异常的时候,这个异常通知才起作用。

1
2
3
4
5
6
7
8
9
10
/**
* @AfterThrowing 将方法标注为异常通知(例外通知)作用于方法抛出异常时
* 可以通过 throwing 设置接收方法异常的信息
* 在参数列表中可以通过具体的异常类型来对指定的异常信息进行操作。例如这里是 NullPointerException 异常
* 即当发生 NullPointerException 异常的时候,这个异常通知才起作用
*/
@AfterThrowing(value = "execution (* com.atguigu.spring.aop.*.*(..))", throwing = "ex")
public void afterThrowingMethod(NullPointerException ex) {
System.out.println("出现异常: " + ex);
}
环绕通知

@Around 将方法标注为环绕通知,其实就是和动态代理一样,可以在方法执行的前后,加上自己想要处理的逻辑。不过参数类型为 ProceedingJoinPoint。proceed 代表执行方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @Around 将方法标注为环绕通知,其实就是和动态代理一样,可以在方法执行的前后,加上自己想要处理的逻辑。
* 不过参数类型为 ProceedingJoinPoint。proceed 代表执行方法。
*/
@Around(value = "execution (* com.atguigu.spring.aop.*.*(..))")
public Object aroundMethos(ProceedingJoinPoint proceedingJoinPoint) { // ProceedingJoinPoint 是 JoinPoint 的子类
Object result;

try {
// 前置通知
System.out.println("前置通知");

// 执行方法
result = proceedingJoinPoint.proceed();

// 返回通知
System.out.println("返回通知");

return result;
} catch (Throwable e) {
e.printStackTrace();

// 异常通知
System.out.println("异常通知");

return -1;
} finally {

// 后置通知
System.out.println("后置通知");
}
}
定义可以重复使用的切入点

当一个切入点表达式可以重复使用的时候,我们可以将其抽取成一个 Pointcut。

1
2
3
4
5
/**
* 创建可以重用的公共切入点
*/
@Pointcut(value = "execution(* com.atguigu.spring.aop.*.*(..))")
public void publicJoinPoint() {}

使用

1
2
@Before(value = "publicJoinPoint()")
public void befordMethod(JoinPoint joinPoint) {}
1
2
@Before(value = "publicJoinPoint()")
public void afterMethos() {}
1
2
@AfterReturning(value = "publicJoinPoint()", returning = "result")
public void afterReturningMethos(JoinPoint joinPoint, Object result) {}
1
2
@AfterThrowing(value = "publicJoinPoint()", throwing = "ex")
public void afterThrowingMethod(NullPointerException ex) {}
切面的优先级

在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。可以用 @Order 注解指定切面的优先级大小,值越小,优先级越高,默认值为 int 的最大值。

新定义一个切面类, 并在其中定一个前置通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.spring.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TestAspectOrder {
@Before(value = "execution(* com.atguigu.spring.aop.*.*(..))")
public void befordMethod() {
System.out.println("TestAspectOrder - 前置通知");
}
}

因为我们在 MyLoggerAspect 中也有一个前置通知,也是作用于切入点 execution(* com.atguigu.spring.aop.*.*(..)) 上的。所以这里就存在一个优先级的问题,可以加上 @Order 来指定切面的优先级。

1
2
3
4
5
6
@Component
@Aspect // 这个注解的作用是标示当前类为切面
@Order(1) // 指定切面优先级
public class MyLoggerAspect {
....
}

这时,因为 TestAspectOrder 没有设置优先级,而 MyLoggerAspect 的优先级是 1,所以就会执行 MyLoggerAspect 中的前置通知。

Spring 原生 AOP 的实现

除了使用 AspectJ 注解声明切面,Spring 也支持在 bean 配置文件中声明切面。这种声明是通过 aop 名称空间中的 XML 元素完成的。而基于 XML 的配置则是 Spring 专有的。

同样定义 MathI 接口和 MathImpl 接口实现类,并新建 MyLogger 类作为切面类。

1
2
3
4
5
6
7
8
9
10
package com.atguigu.spring.aopxml;

import org.springframework.stereotype.Component;

@Component
public class MyLogger {
public void before() {
System.out.println("前置通知");
}
}

新建 aop_xml 配置文件进行 aop 配置。xml 中首先要进行组件的扫描,其次使用 ref 关联切面类,最后定义各种类型的通知。同时也支持将切入点表达式进行抽取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

<!-- 扫描组件 -->
<context:component-scan base-package="com.atguigu.spring.aopxml"></context:component-scan>

<aop:config>
<aop:aspect ref="myLogger"> <!-- myLogger 是切面类的 bean id(类名首字母小写) -->
<!-- 方式1 -->
<!-- <aop:before method="before" pointcut="execution(* com.atguigu.spring.aopxml.*.*(..))"/> -->

<!-- 方式2 -->
<aop:pointcut expression="execution(* com.atguigu.spring.aopxml.*.*(..))" id="pointcut"/>
<aop:before method="before" pointcut-ref="pointcut"></aop:before>
</aop:aspect>
</aop:config>
</beans>

新建测试类进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.spring.aopxml;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("aop_xml.xml");
MathI mathI = applicationContext.getBean("mathImpl", MathI.class);
int result = mathI.add(1, 1);
System.out.println(result);
/**
* 前置通知
2
*/
}
}

代码地址